﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;


namespace Terrain44
{
    public struct VertexTerrain
    {
        public Vector3 Position;
        public Vector3 Normal;
        public Vector4 TextureCoordinate;
        public Vector4 TexWeightsMoon;
        public Vector4 TexWeightsMars;
        public Vector4 TexWeightsLava;

        public static readonly int SizeInBytes = (3 + 3 + 4 + 4 + 4 + 4) * sizeof(float);
        public static readonly VertexDeclaration vertexDeclaration = new VertexDeclaration(
         
             new VertexElement( 0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0 ),
             new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0 ),
             new VertexElement(sizeof(float) * 6, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 0 ),
             new VertexElement(sizeof(float) * 10, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1 ),
             new VertexElement(sizeof(float) * 14, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 2),
             new VertexElement(sizeof(float) * 18, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 3)
         );
        
    }

    class Terrain
    {
        private const float ZONE_MERGE_AREA_FACTOR = 0.3f;

        private const float MOVING_SPEED = 0.15f;
        
        private Texture2D heightMap;
        private Effect effect;
        GraphicsDevice device;

        private Matrix worldMatrix;
        private Matrix viewMatrix;
        private Matrix projectionMatrix;

        private VertexTerrain[] vertices;
        private int[] indices;

        private int terrainWidth;
        private int terrainHeight;
        private const int numberOfZones = 3;
        private const int numberOfAreas = 4;

        private float[,] heightData;
        private Texture2D[] terrainTextures;

        private float move = 0.0f;

        VertexBuffer deviceVertexBuffer;
        IndexBuffer deviceIndexBuffer;

        //Konstruktor
        public Terrain(Texture2D heightMap, Texture2D[] terrainTextures, Effect effect, GraphicsDevice device)
        {
            this.effect = effect;
            this.terrainTextures = terrainTextures;
            this.heightMap = heightMap;
            this.device = device;

           
            LoadHeightData(heightMap);
            SetUpVertices();
            SetUpIndices();
            CalculateNormals();
            SetUpCamera();

            //CopyToBuffers();
        }


        public void DrawTerrain()
        {
            //ZBuffer leeren
            device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);

            device.Indices = deviceIndexBuffer;
            device.SetVertexBuffer(deviceVertexBuffer);






            //alle dreiecke zeichnen, auch die die gegen den uhrzeigersinn                  (im spiel dann löschen!!!!)
            RasterizerState rs = new RasterizerState();
            rs.CullMode = CullMode.None;
            //Dreiecke nicht ausfüllen
            //rs.FillMode = FillMode.WireFrame;
            device.RasterizerState = rs;

            //Effekt setzen
            effect.CurrentTechnique = effect.Techniques["TerrainTextures"];
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();
            }
            //world, view und projection matrizen den effekt übergeben
            effect.Parameters["xView"].SetValue(viewMatrix);
            effect.Parameters["xProjection"].SetValue(projectionMatrix);
            effect.Parameters["xWorld"].SetValue(worldMatrix);


            effect.Parameters["xTexture0"].SetValue(terrainTextures[7]);
            effect.Parameters["xTexture1"].SetValue(terrainTextures[6]);
            effect.Parameters["xTexture2"].SetValue(terrainTextures[5]);
            effect.Parameters["xTexture3"].SetValue(terrainTextures[4]);
            
            
            effect.Parameters["xTexture4"].SetValue(terrainTextures[11]);
            effect.Parameters["xTexture5"].SetValue(terrainTextures[10]);
            effect.Parameters["xTexture6"].SetValue(terrainTextures[9]);
            effect.Parameters["xTexture7"].SetValue(terrainTextures[8]);
            
            effect.Parameters["xTexture8"].SetValue(terrainTextures[3]);
            effect.Parameters["xTexture9"].SetValue(terrainTextures[2]);
            effect.Parameters["xTexture10"].SetValue(terrainTextures[1]);
            effect.Parameters["xTexture11"].SetValue(terrainTextures[0]);
            


            //Licht einschalten
            Vector3 lightDirection = new Vector3(1.0f, -1.0f, -1.0f);
            lightDirection.Normalize();
            effect.Parameters["xLightDirection"].SetValue(lightDirection);
            effect.Parameters["xAmbient"].SetValue(0.1f);
            effect.Parameters["xEnableLighting"].SetValue(true);

            //Zeichnen
            device.DrawUserIndexedPrimitives<VertexTerrain>(PrimitiveType.TriangleList, vertices, 0, vertices.Length, indices, 0, indices.Length / 3, VertexTerrain.vertexDeclaration);

            
        
        }


        public void UpdateTerrain()
        {
            move += MOVING_SPEED;
            UpdateWorldMatrix();
        }


        //Höhendaten aus der Heightmap auslesen und im HeightData array speichern
        private void LoadHeightData(Texture2D heightMapTexture)
        {
            terrainWidth = heightMapTexture.Width;
            terrainHeight = heightMapTexture.Height;

            Color[] heightMapColors = new Color[terrainWidth * terrainHeight];

            heightMapTexture.GetData(heightMapColors);

            //height array mit den farbwerten für rot (abgescchwächst, deshalb / 5) befüllen
            heightData = new float[terrainWidth, terrainHeight];
            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainHeight; y++)
                {
                    heightData[x, y] = heightMapColors[x + (y * terrainWidth)].R / 5;
                }
            }
        }


        //Vertexes erstellen
        private void SetUpVertices()
        {
            //Höhenzonen
            float minHeight = float.MaxValue;
            float maxHeight = float.MinValue;
            float areaHeight;
            float zoneLength;
            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainHeight; y++)
                {
                    if (heightData[x, y] < minHeight)
                        minHeight = heightData[x, y];
                    if (heightData[x, y] > maxHeight)
                        maxHeight = heightData[x, y];
                }
            }

            
            areaHeight = (maxHeight - minHeight) / (numberOfAreas - 1);

            float zoneMergeArea = (terrainHeight / numberOfZones) * ZONE_MERGE_AREA_FACTOR;
            zoneLength = (terrainHeight - (numberOfZones - 1) * zoneMergeArea) / numberOfZones;

            //vertexe erstellen, position und textur
            vertices = new VertexTerrain[terrainWidth * terrainHeight];
            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainHeight; y++)
                {
                    vertices[x + (y * terrainWidth)].Position = new Vector3(x, heightData[x, y], -y);

                    vertices[x + y * terrainWidth].TextureCoordinate.X = (float)x / 30.0f;
                    vertices[x + y * terrainWidth].TextureCoordinate.Y = (float)y / 30.0f;

                    float zoneMoonfactor = 0;
                    float zoneMarsFactor = 0;
                    float zoneLavaFactor = 0;

                    zoneMoonfactor = MathHelper.Clamp( 1 - (y - zoneLength) / zoneMergeArea , 0 , 1);
                    zoneLavaFactor = MathHelper.Clamp( (y - (2 * zoneLength + zoneMergeArea)) / zoneMergeArea , 0 , 1);
                    zoneMarsFactor = MathHelper.Clamp( ((y - zoneLength) / zoneMergeArea) , 0, 1) - zoneLavaFactor;

                  

                    vertices[x + y * terrainWidth].TexWeightsMoon.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - minHeight) / (areaHeight), 0, 1) * zoneMoonfactor;
                    vertices[x + y * terrainWidth].TexWeightsMoon.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - areaHeight) / (areaHeight), 0, 1) * zoneMoonfactor;
                    vertices[x + y * terrainWidth].TexWeightsMoon.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - (2 * areaHeight)) / (areaHeight), 0, 1) * zoneMoonfactor;
                    vertices[x + y * terrainWidth].TexWeightsMoon.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - maxHeight) / (areaHeight), 0, 1) * zoneMoonfactor;
                    
                    vertices[x + y * terrainWidth].TexWeightsMars.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - minHeight) / (areaHeight), 0, 1) * zoneMarsFactor;
                    vertices[x + y * terrainWidth].TexWeightsMars.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - areaHeight) / (areaHeight), 0, 1) * zoneMarsFactor;
                    vertices[x + y * terrainWidth].TexWeightsMars.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - (2 * areaHeight)) / (areaHeight), 0, 1) * zoneMarsFactor;
                    vertices[x + y * terrainWidth].TexWeightsMars.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - maxHeight) / (areaHeight), 0, 1) * zoneMarsFactor;
                   
                    vertices[x + y * terrainWidth].TexWeightsLava.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - minHeight) / (areaHeight), 0, 1) * zoneLavaFactor;
                    vertices[x + y * terrainWidth].TexWeightsLava.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - areaHeight) / (areaHeight), 0, 1) * zoneLavaFactor;
                    vertices[x + y * terrainWidth].TexWeightsLava.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - (2 * areaHeight)) / (areaHeight), 0, 1) * zoneLavaFactor;
                    vertices[x + y * terrainWidth].TexWeightsLava.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - maxHeight) / (areaHeight), 0, 1) * zoneLavaFactor;




                    float total = vertices[x + y * terrainWidth].TexWeightsMoon.X;
                    total += vertices[x + y * terrainWidth].TexWeightsMoon.Y;
                    total += vertices[x + y * terrainWidth].TexWeightsMoon.Z;
                    total += vertices[x + y * terrainWidth].TexWeightsMoon.W;

                    total += vertices[x + y * terrainWidth].TexWeightsMars.X;
                    total += vertices[x + y * terrainWidth].TexWeightsMars.Y;
                    total += vertices[x + y * terrainWidth].TexWeightsMars.Z;
                    total += vertices[x + y * terrainWidth].TexWeightsMars.W;

                    total += vertices[x + y * terrainWidth].TexWeightsLava.X;
                    total += vertices[x + y * terrainWidth].TexWeightsLava.Y;
                    total += vertices[x + y * terrainWidth].TexWeightsLava.Z;
                    total += vertices[x + y * terrainWidth].TexWeightsLava.W;

                    vertices[x + y * terrainWidth].TexWeightsMoon.X /= total;
                    vertices[x + y * terrainWidth].TexWeightsMoon.Y /= total;
                    vertices[x + y * terrainWidth].TexWeightsMoon.Z /= total;
                    vertices[x + y * terrainWidth].TexWeightsMoon.W /= total;

                    vertices[x + y * terrainWidth].TexWeightsMars.X /= total;
                    vertices[x + y * terrainWidth].TexWeightsMars.Y /= total;
                    vertices[x + y * terrainWidth].TexWeightsMars.Z /= total;
                    vertices[x + y * terrainWidth].TexWeightsMars.W /= total;

                    vertices[x + y * terrainWidth].TexWeightsLava.X /= total;
                    vertices[x + y * terrainWidth].TexWeightsLava.Y /= total;
                    vertices[x + y * terrainWidth].TexWeightsLava.Z /= total;
                    vertices[x + y * terrainWidth].TexWeightsLava.W /= total;

                   
                }
            }
        }


        //legt Indizes der vertexes fest
        private void SetUpIndices()
        {
            indices = new int[(terrainWidth - 1) * (terrainHeight - 1) * 6];
            int counter = 0;
            for (int y = 0; y < terrainHeight - 1; y++)
            {
                for (int x = 0; x < terrainWidth - 1; x++)
                {
                    int lowerLeft = x + y * terrainWidth;
                    int lowerRight = (x + 1) + y * terrainWidth;
                    int topLeft = x + (y + 1) * terrainWidth;
                    int topRight = (x + 1) + (y + 1) * terrainWidth;

                    indices[counter++] = topLeft;
                    indices[counter++] = lowerRight;
                    indices[counter++] = lowerLeft;

                    indices[counter++] = topLeft;
                    indices[counter++] = topRight;
                    indices[counter++] = lowerRight;


                }
            }
        }


        //Kamera und Projektionseinstellungen
        private void SetUpCamera()
        {
            //0.150.80
            viewMatrix = Matrix.CreateLookAt(new Vector3(0, 150, 80), new Vector3(0, 0, -10), new Vector3(0, 1, 0));
            

            projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 1.0f, 1000.0f);
        }


        private void UpdateWorldMatrix()
        {
            worldMatrix = Matrix.CreateTranslation(new Vector3(0, 0, move)) * Matrix.CreateTranslation(new Vector3(-terrainWidth / 2, 0, terrainWidth / 2));

        }

        private void CalculateNormals()
        {
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal = new Vector3(0, 0, 0);

            for (int i = 0; i < indices.Length / 3; i++)
            {
                int index1 = indices[i * 3];
                int index2 = indices[i * 3 + 1];
                int index3 = indices[i * 3 + 2];

                Vector3 side1 = vertices[index1].Position - vertices[index3].Position;
                Vector3 side2 = vertices[index1].Position - vertices[index2].Position;
                Vector3 normal = Vector3.Cross(side1, side2);

                vertices[index1].Normal += normal;
                vertices[index2].Normal += normal;
                vertices[index3].Normal += normal;


            }
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal.Normalize();
        }

        //Daten in speicher der grafikkarte laden
        private void CopyToBuffers()
        {
            deviceVertexBuffer = new VertexBuffer(device, VertexTerrain.vertexDeclaration, vertices.Length, BufferUsage.WriteOnly);
            deviceVertexBuffer.SetData(vertices);

            deviceIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly);
            deviceIndexBuffer.SetData(indices);
        }



    }
}
